package jmine.tec.persist.hibernate.merger;

import java.io.Serializable;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Map.Entry;

import jmine.tec.persist.hibernate.initializer.LazyResolverEnum;

import org.hibernate.EntityMode;
import org.hibernate.engine.CascadingAction;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.type.CollectionType;
import org.hibernate.type.ComponentType;
import org.hibernate.type.MapType;
import org.hibernate.type.Type;

import bancosys.tec.persist.Persistable;

/**
 * Faz o merge de uma entidade desassociada com a session, retornando uma instancia pronta para ser salva. A instância para ser salva é uma
 * "deep copy", não compartilha instâncias stateful com a entidade original, portanto uma tentativa de "save" que dá errado após o início
 * não deixa a entidade original em um estado inválido.
 * 
 * @author gabriel.silva
 * @author lundberg
 * @author wellington.santos
 */
public class EntityMerger {

    private EntityLoader entityLoader;

    private SessionFactoryImplementor sessionFactoryImplementor;

    /**
     * Executa o merge de uma entidade desassociada com a session.
     * 
     * @param <T> tipo de entidade
     * @param entity entity
     * @return merged entity
     */
    @SuppressWarnings("unchecked")
    public <T extends Persistable> T merge(T entity) {
        return (T) this.mergeEntity(new IdentityHashMap<Object, Object>(), entity, true);
    }

    /**
     * Faz o merge da entidade
     * 
     * @param visited visited
     * @param aEntity entity
     * @param isCascade is cascade?
     * @return merged entity
     */
    private Object mergeEntity(Map<Object, Object> visited, Object aEntity, boolean isCascade) {
        if (aEntity == null) {
            return null;
        }

        LazyResolverEnum lazyResolver = LazyResolverEnum.forObject(aEntity);
        boolean isUninitialized = lazyResolver.isUninitialized(aEntity);
        Object entity = lazyResolver.resolve(aEntity);
        Class entityClass = entity.getClass();

        AbstractEntityPersister loadedMetadata = (AbstractEntityPersister) this.sessionFactoryImplementor.getClassMetadata(entityClass);
        if (loadedMetadata == null) {
            return entity;
        }
        if (visited.containsKey(entity)) {
            return visited.get(entity);
        }

        Serializable id = loadedMetadata.getIdentifier(entity, EntityMode.POJO);
        final Object loadedObject = this.getLoadedObject(lazyResolver, entityClass, id);

        if (entity instanceof Persistable) {
            ((Persistable) loadedObject).setController(((Persistable) entity).getController());
        }

        visited.put(entity, loadedObject);

        if (id != null && (isUninitialized || !isCascade)) {
            return loadedObject;
        }

        String[] properties = loadedMetadata.getPropertyNames();
        for (int i = 0; i < properties.length; i++) {
            String propertyName = properties[i];
            Type propertyType = loadedMetadata.getPropertyType(propertyName);
            Object editedValue = loadedMetadata.getPropertyValue(entity, propertyName, EntityMode.POJO);
            Object loadedValue = loadedMetadata.getPropertyValue(loadedObject, propertyName, EntityMode.POJO);
            boolean isCascadeProperty = loadedMetadata.getCascadeStyle(i).doCascade(CascadingAction.SAVE_UPDATE);
            Object copy = this.mergeProperty(visited, editedValue, loadedValue, propertyType, isCascadeProperty);
            loadedMetadata.setPropertyValue(loadedObject, propertyName, copy, EntityMode.POJO);
        }
        return loadedObject;

    }

    /**
     * Obtains a loaded instance
     * 
     * @param lazyResolver lazyResolver
     * @param entityClass entityClass
     * @param id id
     * @return loaded instance
     */
    private Object getLoadedObject(LazyResolverEnum lazyResolver, Class entityClass, Serializable id) {
        if (id == null) {
            return this.entityLoader.create(entityClass);
        }
        Object loadedObject = this.entityLoader.get(entityClass, id);
        return LazyResolverEnum.forObject(loadedObject).resolve(loadedObject);
    }

    /**
     * Copies a single property
     * 
     * @param visited visited
     * @param editedValue editedValue
     * @param loadedValue loadedValue
     * @param propertyType type
     * @param isCascade isCascade
     * @return merged property value
     */
    @SuppressWarnings("unchecked")
    private Object mergeProperty(Map<Object, Object> visited, Object editedValue, Object loadedValue, Type propertyType, boolean isCascade) {
        if (loadedValue != null && LazyResolverEnum.forObject(editedValue).isUninitialized(editedValue)) {
            // se o valor editado é lazy e não inicializou então nunca foi modificado
            return loadedValue;
        }
        if (propertyType.isComponentType()) {
            // embedded beans
            ComponentType componentType = (ComponentType) propertyType;
            Type[] componentPropertyTypes = componentType.getSubtypes();
            Object[] componentPropertyValues = componentType.getPropertyValues(editedValue, EntityMode.POJO);
            Object[] loadPropertyValues = componentType.getPropertyValues(loadedValue, EntityMode.POJO);
            Object[] resultPropertyValues = componentPropertyValues;
            for (int j = 0; j < componentPropertyTypes.length; j++) {
                boolean isCascadeProperty = componentType.getCascadeStyle(j).doCascade(CascadingAction.SAVE_UPDATE);
                resultPropertyValues[j] =
                        this.mergeProperty(visited, componentPropertyValues[j], loadPropertyValues[j], componentPropertyTypes[j],
                                isCascadeProperty);
            }
            componentType.setPropertyValues(loadedValue, resultPropertyValues, EntityMode.POJO);
            return loadedValue;
        } else if (propertyType.isCollectionType()) {
            if (propertyType instanceof MapType) {
                // map values
                MapType mapType = (MapType) propertyType;
                CollectionPersister mapPersister = this.sessionFactoryImplementor.getCollectionPersister(mapType.getRole());
                Map loadedMap = (Map) (loadedValue == null ? mapType.instantiate(0) : loadedValue);
                loadedMap.clear();
                if (editedValue != null) {
                    for (Entry<?, ?> editedEntry : ((Map<?, ?>) editedValue).entrySet()) {
                        Object mergedKey = this.mergeProperty(visited, editedEntry.getKey(), null, mapPersister.getIndexType(), isCascade);
                        Object mergedValue =
                                this.mergeProperty(visited, editedEntry.getValue(), null, mapPersister.getElementType(), isCascade);
                        loadedMap.put(mergedKey, mergedValue);
                    }
                }
                return loadedMap;
            } else {
                // collections e arrays
                CollectionType collectionType = (CollectionType) propertyType;
                Collection loadedCollection = (Collection) (loadedValue == null ? collectionType.instantiate(0) : loadedValue);
                Collection editedCollection = (Collection) editedValue;
                loadedCollection.clear();
                if (editedValue != null) {
                    for (Object editedElement : editedCollection) {
                        Object mergedElement =
                                this.mergeProperty(visited, editedElement, null,
                                        collectionType.getElementType(this.sessionFactoryImplementor), isCascade);
                        loadedCollection.add(mergedElement);
                    }
                }
                return loadedCollection;
            }
        } else if (propertyType.isAssociationType()) {
            // outros beans e entidades
            return this.mergeEntity(visited, editedValue, isCascade);
        } else {
            // valores imutáveis
            return editedValue;
        }
    }

    /**
     * @param session the session to set
     */
    public void setSessionFactoryImplementor(SessionFactoryImplementor session) {
        this.sessionFactoryImplementor = session;
    }

    /**
     * @param mergerLoader the mergerLoader to set
     */
    public void setEntityLoader(EntityLoader mergerLoader) {
        this.entityLoader = mergerLoader;
    }

}
